package net.wzw.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import net.wzw.constant.CacheKeyConstant;
import net.wzw.constant.RedisKeyConstant;
import net.wzw.entity.LoginUser;
import net.wzw.entity.UserLoginEntity;
import net.wzw.entity.UserRegisterEntity;
import net.wzw.entity.UserRequestEntity;
import net.wzw.enums.BizCodes;
import net.wzw.enums.SendCodes;
import net.wzw.exception.BizException;
import net.wzw.interceptor.LoginInterceptor;
import net.wzw.mapper.UserMapper;
import net.wzw.model.UserDO;
import net.wzw.redis.RedisCache;
import net.wzw.redis.RedisLock;
import net.wzw.service.CaptchaService;
import net.wzw.service.UserService;
import net.wzw.utils.ApiResult;
import net.wzw.utils.CommonUtil;
import net.wzw.utils.JWTUtil;
import net.wzw.utils.JsonUtil;
import net.wzw.vo.UserVO;
import org.apache.commons.codec.digest.Md5Crypt;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author wzw
 * @since 2024-08-11
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {

    /**
     * 注入验证码服务
     */
    @Autowired
    private CaptchaService captchaService;

    /**
     * 注入用户 mapper
     */
    @Autowired
    private UserMapper userMapper;

    /**
     * 注入 redis
     */
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 注入 redis 缓存工具类
     */
    @Autowired
    private RedisCache redisCache;

    /**
     * 注入 redis
     */
    @Autowired
    private RedisLock redisLock;

    /**
     * 用户注册核心逻辑
     * 1、检查邮箱验证码是否正确
     * 2、密码安全、高并发下账号唯一性检测
     * 3、入库
     * @param userRegisterEntity
     * @return
     */
    @Override
    public ApiResult register(UserRegisterEntity userRegisterEntity) {
        boolean checkCode = false;

        /**
         * 1、检查邮箱验证码是否正确
         */
        // 判断邮箱是否为空
        if(StringUtils.isNotBlank(userRegisterEntity.getMail())){
            // 如果不为空，则检查验证码是否正确
            checkCode = captchaService.checkCode(SendCodes.USER_REGISTER,
                    userRegisterEntity.getMail(),
                    userRegisterEntity.getCode());
        }
        // 如果验证码不正确，之间返回验证码错误
        if(!checkCode){
            // TODO 手机号验证码此处还未处理
            return ApiResult.doResult(BizCodes.USER_CODE_EMAIL_ERROR);
        }

        UserDO userDO = new UserDO();
        BeanUtils.copyProperties(userRegisterEntity, userDO);
        userDO.setCreateTime(new Date());
        userDO.setUpdateTime(new Date());

        /**
         * 1、密码安全，设置密码
         */
        // 生成盐 salt
        userDO.setSecret("$1$" + CommonUtil.getRandomSalt(8));
        // 生成密码  密码 + 盐 slat
        String pwd = Md5Crypt.md5Crypt(userRegisterEntity.getPassword().getBytes(), userDO.getSecret());
        userDO.setPwd(pwd);

        // 账号唯一性检测 TODO
        if (checkUnique(userDO.getPhone())) {
            int row = userMapper.insert(userDO);
            log.info("用户注册模块-影响行数：{}, 注册成功:{}", row, userDO.toString());
            // 新用户注册成功，需要初始化信息，发送福利等  TODO
            initTask(userDO);
            return ApiResult.doSuccess();
        } else {
            // 用户账号已存在，无法注册
            return ApiResult.doResult(BizCodes.USER_ACCOUNT_EXIST);
        }
    }

    /**
     * 检测用户账号唯一性
     * @param phone
     * @return
     */
    private boolean checkUnique(String phone) {
        QueryWrapper queryWrapper = new QueryWrapper<UserDO>().eq("phone", phone);

        List<UserDO> list = userMapper.selectList(queryWrapper);
        log.info("用户注册模块-查询手机号结果：{}", list.toArray());
        return list.size() > 0 ? false : true;
    }

    /**
     * 用户注册成功，需要初始化信息，发送福利等
     * @param userDO
     */
    private void initTask(UserDO userDO){
        // TODO
    }

    /**
     * 1、查找手机号是否已经存在，存在表示已注册，否则未注册
     * 2、匹配密码是否正确，正确则登录成功，否则提示账号/密码错误
     * 3、登录成功 token 解决方案 TODO
     * @param userLoginEntity
     * @return
     */
    @Override
    public ApiResult login(UserLoginEntity userLoginEntity) {
        /**
         * 1、查找手机号是否存在
         */
        List<UserDO>  userList = userMapper.selectList(new QueryWrapper<UserDO>().eq("phone", userLoginEntity.getPhone()));
        if (userList != null && userList.size() == 1) {
            // 表示该手机号已经注册了
            UserDO userDO = userList.get(0);
            String pwd = Md5Crypt.md5Crypt(userLoginEntity.getPassword().getBytes(), userDO.getSecret());
            // 如果请求的密码加密后跟数据库的匹配
            if (pwd.equals(userDO.getPwd())) {
                // 登录成功，生成 Token
                LoginUser loginUser = new LoginUser();
                //LoginUser loginUser = LoginUser.builder().build();
                // 拷贝
                BeanUtils.copyProperties(userDO, loginUser);
                // 生成 JWT Token，过期时间
                Map<String,Object> tokenInfo = JWTUtil.generatorJWT(loginUser);
                // 设置 RefreshToken 到 Redis 中，过期时间为 30 天
                String refreshCacheKey = String.format(CacheKeyConstant.REFRESH_TOKEN_KEY, tokenInfo.get("RefreshToken").toString());
                redisTemplate.opsForValue().set(refreshCacheKey, "1", JWTUtil.REFRESH_EXPIRE, TimeUnit.MILLISECONDS);
                log.info("用户登录模块-请求参数：{}，登录成功：{}，生成 token：{}, refreshCacheKey：{}", userLoginEntity.toString(), userDO.toString(), tokenInfo, refreshCacheKey);
                return ApiResult.doSuccess(tokenInfo);
            } else {
                // 账号/密码错误
                return ApiResult.doResult(BizCodes.USER_ACCOUNT_PWD_ERROR);
            }
        } else {
            // 未注册
            return ApiResult.doResult(BizCodes.USER_ACCOUNT_UNREGISTER);
        }
    }

    /**
     * 用户刷新 token
     * @param params
     * @return
     */
    @Override
    public ApiResult refreshToken(Map<String, Object> params) {
        // 1、先查找 redis 是否存在 refreshToken
        String refreshCacheKey = String.format(CacheKeyConstant.REFRESH_TOKEN_KEY, params.get("refresh_token").toString());
        String refreshCacheVal = redisTemplate.opsForValue().get(refreshCacheKey);
        log.info("用户刷新模块-参数：{}, 缓存验证码key：{}，缓存验证码val：{}", params, refreshCacheKey, refreshCacheVal);
        /**
         * 如果不为空，则判断是否匹配
         */
        if (StringUtils.isNotBlank(refreshCacheVal)) {
            // 2、如果存在，解密 accessToken
            Claims claims = JWTUtil.checkJWT(params.get("access_token").toString());
            if (claims == null) {
                // 无法解密提示未登录
                return ApiResult.doResult(BizCodes.USER_ACCOUNT_UNLOGIN);
            }
            // 3、如果可以解密 accessToken， 则重新生成 accessToken 等信息返回
            long userId = Long.valueOf(claims.get("id").toString());
            List<UserDO>  userList = userMapper.selectList(new QueryWrapper<UserDO>().eq("id", userId));
            if (userList != null && userList.size() == 1) {
                UserDO userDO = userList.get(0);
                // 登录成功，生成 Token
                LoginUser loginUser = new LoginUser();
                //LoginUser loginUser = LoginUser.builder().build();
                // 拷贝
                BeanUtils.copyProperties(userDO, loginUser);
                // 生成 JWT Token，过期时间
                Map<String,Object> tokenInfo = JWTUtil.generatorJWT(loginUser);
                // 4、设置 RefreshToken 到 Redis 中，过期时间为 30 天
                String refreshNewCacheKey = String.format(CacheKeyConstant.REFRESH_TOKEN_KEY, tokenInfo.get("RefreshToken").toString());
                redisTemplate.opsForValue().set(refreshNewCacheKey, "1", JWTUtil.REFRESH_EXPIRE, TimeUnit.MILLISECONDS);
                log.info("用户刷新模块-请求参数：{}，登录成功：{}，生成 token：{}, refreshCacheKey：{}", params, userDO.toString(), tokenInfo, refreshCacheKey);
                // TODO 可以删除旧的 refreshToken
                return ApiResult.doSuccess(tokenInfo);
            } else {
                // 无法解密提示未登录
                return ApiResult.doResult(BizCodes.USER_ACCOUNT_UNLOGIN);
            }
        } else {
            // refreshToken 不存在
            return ApiResult.doResult(BizCodes.USER_REFRESH_TOKEN_EMPTY);
        }
    }

    /**
     * 查询个人信息
     * @return
     */
    @Override
    public UserVO getUserDetail() {
        // 这里我们如何获取登录用户信息呢，前面我们通过 ThreadLocal 进行了用户信息的传递
        LoginUser loginUser = LoginInterceptor.threadLocal.get();

        // 1、先读取 Redis 缓存
        UserVO user = getUserInfoFromCache(loginUser.getId());
        if (user != null) {
            return user;
        }

        // 2、缓存为空再从数据库中获取
        return getUserInfoFromDB(loginUser.getId());
    }

    /**
     * 从缓存中读取对应数据
     * @param userId
     * @return
     */
    private UserVO getUserInfoFromCache(Long userId) {
        // redis 缓存key
        String userInfoKey = RedisKeyConstant.USER_INFO_PREFIX + userId;
        String userInfo = redisCache.get(userInfoKey);
        log.info("用户模块-从缓存中获取信息，userid：{}，value：{}", userId, userInfo);
        /**
         * 如果不为空
         */
        if (org.springframework.util.StringUtils.hasLength(userInfo)) {
            // 当读到空数据时返回空，防止缓存穿透
            if (Objects.equals(redisCache.EMPTY_CACHE, userInfo)) {
                return null;
            }
            // 缓存自动延期 2天 + 随机几个小时
            redisCache.expire(userInfoKey, RedisCache.generateCacheExpire());
            UserVO userVo = new UserVO();
            BeanUtils.copyProperties(JsonUtil.json2Object(userInfo, UserVO.class), userVo);
            log.info("用户模块-解析缓存信息，userid：{}，userInfo：{}，userVo：{}", userId, userInfo, userVo);
            return userVo;
        }

        return null;
    }

    /**
     * 从数据库中查询
     * @param userId
     * @return
     */
    public UserVO getUserInfoFromDB(Long userId) {
        // 这里增加分布式锁，同时间只能一个用户成功，避免重复请求
        /**
         * 1、这里我们将读写锁改成同一把锁：RedisKeyConstant.USER_UPDATE_LOCK_PREFIX，让读写操作进行互斥
         */
        String userUpdateLockKey = RedisKeyConstant.USER_UPDATE_LOCK_PREFIX + userId;
        boolean lock = false;
        try {
            lock = redisLock.tryLock(userUpdateLockKey, RedisCache.USER_UPDATE_LOCK_TIMEOUT);
        } catch (InterruptedException e) {
            // 锁异常后再次从缓存中查询
            UserVO user = getUserInfoFromCache(userId);
            if (user != null) {
                return user;
            }
            log.error("尝试加锁异常，异常信息：{}，异常栈信息：{}", e.getMessage(), e);
            throw new BizException(BizCodes.USER_INFO_LOCK_FAIL);
        }

        if (!lock) {
            // 锁超时失败后再次从缓存中查询
            UserVO user = getUserInfoFromCache(userId);
            if (user != null) {
                return user;
            }
            log.info("用户缓存为空，从数据库获取用户信息获取锁失败，user:{}", userId);
            throw new BizException(BizCodes.USER_INFO_LOCK_FAIL);
        }

        try {
            String userInfoKey = RedisKeyConstant.USER_INFO_PREFIX + userId;
            /**
             * 2、可能当加锁之后，其实已经刚好有线程更新了最新的数据，所以这里进行一下 double check
             */
            // 锁失败后再次从缓存中查询
            UserVO user = getUserInfoFromCache(userId);
            if (user != null) {
                return user;
            }

            log.info("用户缓存为空，从数据库获取用户信息，user:{}", userId);

            // 从数据库中获取用户信息
            UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().eq("id", userId));
            // 如果从数据库中读取为空数据
            if (Objects.isNull(userDO)) {
                // 设置一个空数据到缓存，过期时间为穿透的过期时间，30~100秒
                redisCache.set(userInfoKey, redisCache.EMPTY_CACHE, RedisCache.generateCachePenetrationExpire());
                return null;
            }
            // 这里我们返回 userVo 的信息, 需要通过拷贝 userDO 到 userVo 中
            UserVO userVo = new UserVO();
            BeanUtils.copyProperties(userDO, userVo);

            //从数据库中获取用户关系表
           // UserRelationDO userRelationDO
           // 写入缓存，缓存过期时间 2天 + 随机几小时
           redisCache.setCache(userInfoKey, userVo, RedisCache.generateCacheExpire());
           return userVo;
        } finally {
            // 释放锁
            redisLock.unlock(userUpdateLockKey);
        }
    }

    /**
     * 修改用户信息
     * @param userRequestEntity
     * @return
     */
    @Override
    public ApiResult save(UserRequestEntity userRequestEntity) {
        // 获取用户信息
        LoginUser loginUser = LoginInterceptor.threadLocal.get();
        // 这里增加分布式锁，同一个用户同时间只能操作一次，避免重复请求
        String userUpdateLockKey = RedisKeyConstant.USER_UPDATE_LOCK_PREFIX + loginUser.getId();
        boolean lock = redisLock.lock(userUpdateLockKey);

        if (!lock) {
            log.info("修改用户信息获取锁失败，user:{}", loginUser.toString());
            throw new BizException(BizCodes.USER_UPDATE_LOCK_FAIL);
        }

        try {
            // 更新用户数据
            UserDO userDO = new UserDO();
            BeanUtils.copyProperties(userRequestEntity, userDO);
            // 重新设置修改时间
            userDO.setUpdateTime(new Date());
            userDO.setId(loginUser.getId());

            // 新增收货地址
            int rows = userMapper.update(userDO, new QueryWrapper<UserDO>().eq("id", loginUser.getId()));
            log.info("用户模块-修改用户信息：rows={}，data={}", rows, userDO);

            // 写入缓存
            if (rows == 1) {
                // 缓存过期时间
                redisCache.setCache(RedisKeyConstant.USER_INFO_PREFIX + loginUser.getId(), userRequestEntity,
                        RedisCache.generateCacheExpire());
            }
        } finally {
            // 释放锁
            redisLock.unlock(userUpdateLockKey);
        }

        return null;
    }
}
